﻿using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using System.Collections.Generic;


namespace Logging
{
	/// <summary>
		/// interface to define the methods needed in a loggable item..
		/// </summary>
		public interface ILoggable : IDisposable {
			void Write(string message);
			void Write(string format, params object[] args);
			void WriteLine(string message);
			void WriteLine();
			void WriteLine(string format, params object[] args);
			void WriteStamped(string message);
			void WriteStamped(string format, params object[] args);
			void WriteStampedLine(string message);
			void WriteStampedLine(string format, params object[] args);
			void LogException(Exception error);
			void Flush();
		}


		/// <summary>
		/// Low Level class to allow logging to anything that implements TextWriter (e.g. StreamWriter)
		/// </summary>
		public class TWLogger : ILoggable {

			protected bool Used { get; private set; }
			protected TextWriter mWriter { get; set; }

			protected TWLogger() { }

			public TWLogger(TextWriter aWriter) {
				mWriter = aWriter;
				Used = false;
			}

			#region ILoggable Members

			public void Write(string message) {
				mWriter.Write(message);
				Used = true;
			}

			public void Write(string format, params object[] args) {
				string message = string.Format(format, args);
				Write(message);
			}

			public void WriteLine(string message) {
				mWriter.WriteLine(message);
				Used = true;
			}

			public void WriteLine() {
				WriteLine("");
			}

			public void WriteLine(string format, params object[] args) {
				string message = string.Format(format, args);
				WriteLine(message);
			}

			public void WriteStamped(string message) {
				message = GetTimeStamp() + ":\t" + message;
				Write(message);
			}

			public void WriteStamped(string format, params object[] args) {
				string message = string.Format(format, args);
				message = GetTimeStamp() + ":\t" + message;
				Write(message);
			}

			public void WriteStampedLine(string message) {
				message = GetTimeStamp() + ":\t" + message;
				WriteLine(message);
			}

			public void WriteStampedLine(string format, params object[] args) {
				string message = string.Format(format, args);
				message = GetTimeStamp() + ":\t" + message;
				WriteLine(message);
			}

			public void LogException(Exception error) {
				WriteLine(GetExceptionTextLog(error));
			}

			public void Flush() {
				mWriter.Flush();
			}

			#endregion

			#region Supporting Methods
			protected string GetTimeStamp() {
				return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
			}

			protected string GetExceptionTextLog(Exception doh) {
				StackTrace st = new StackTrace();
				MethodBase invokingMethod = st.GetFrame(2).GetMethod();

				StringBuilder sb = new StringBuilder();

				sb.Append(ExceptionTextLogHeader(invokingMethod));

				int currentLevel = 0;
				while (doh != null) {
					sb.Append("Level " + currentLevel.ToString() + ":" + Environment.NewLine);
					sb.Append('-', 7 + (currentLevel++).ToString().Length);
					sb.Append(Environment.NewLine);
					sb.Append(ExceptionTextLogExceptionMessage(doh));

					doh = doh.InnerException;
				}

				sb.Append(Environment.NewLine + Environment.NewLine);

				return sb.ToString();
			}

			protected string ExceptionTextLogHeader(MethodBase invokingMethod) {
				StringBuilder header = new StringBuilder();

				header.Append("Exception Logger invoked by " + (invokingMethod.DeclaringType == null ? string.Empty : invokingMethod.DeclaringType.FullName + ".") + invokingMethod.Name + Environment.NewLine);

				int firstLineLength = 29 + (invokingMethod.DeclaringType == null ? 0 : invokingMethod.DeclaringType.FullName.Length) + invokingMethod.Name.Length;
				header.Append('-', firstLineLength);
				header.Append(Environment.NewLine);
				header.Append("Logged at: " + DateTime.Now.ToString("HH:mm:ss dd/MM/yy") + Environment.NewLine);
				//include the current stack trace in the entry, but cut out all those levels below the constructor of the LogEntry, which
				//should be 3 stack levels above us.
				StackTrace st = new StackTrace(3);
				if (st.FrameCount == 0)
					header.Append("Current stack trace: [empty]").Append(Environment.NewLine);
				else {
					header.Append("Current stack trace:").Append(Environment.NewLine);
					for (int i = 0; i < st.FrameCount; ++i) {
						MethodBase method = st.GetFrame(i).GetMethod();
						// Write down the declaring type if it exists. It can be null (when failing on a compiled XSLT).
						string methodSignature = (method.DeclaringType == null ? "[no declaring type]" : method.DeclaringType.ToString()) + ": ";
						methodSignature += method.ToString();
						header.Append(methodSignature).Append(Environment.NewLine);
					}
				}
				header.Append(Environment.NewLine);

				return header.ToString();
			}

			protected string ExceptionTextLogExceptionMessage(Exception doh) {
				StringBuilder sb = new StringBuilder();

				sb.Append("Exception type: " + doh.GetType());

				//Beware that this stacktrace will be empty of frames if the supplied exception was
				//never actually thrown.
				StackTrace st = new StackTrace(doh);

				if (st.FrameCount == 0) {
					sb.Append(Environment.NewLine).Append("Exception not yet thrown.");
				}
				else {
					sb.Append(Environment.NewLine).Append("Thrown by method: " + st.GetFrame(0).GetMethod().ToString());
					System.Type declaringType = st.GetFrame(0).GetMethod().DeclaringType;
					sb.Append(Environment.NewLine).Append("Of class: " + (declaringType == null ? "[no declaring type]" : declaringType.FullName));
				}
				sb.Append(Environment.NewLine);

				if (st.FrameCount == 0)
					sb.Append("Exception stack trace: [empty]").Append(Environment.NewLine);
				else {
					sb.Append("Exception stack trace:").Append(Environment.NewLine);
					for (int i = 0; i < st.FrameCount; ++i) {
						MethodBase method = st.GetFrame(i).GetMethod();
						// Write down the declaring type if it exists. It can be null (when failing on a compiled XSLT).
						string methodSignature = (method.DeclaringType == null ? "[no declaring type]" : method.DeclaringType.ToString()) + ": ";
						methodSignature += method.ToString();
						sb.Append(methodSignature).Append(Environment.NewLine);
					}
				}

				sb.Append(Environment.NewLine).Append("Message: " + doh.Message + Environment.NewLine + Environment.NewLine);
				/*
				if(doh is SqlException)
					sb.Append(ExceptionTextLogExceptionMessage((SqlException)doh));
				else if(doh is WebException) {
					sb.Append(ExceptionTextLogExceptionMessage((WebException)doh));
				}
				 */

				return sb.ToString();
			}
			#endregion

			#region IDisposable Members

			public virtual void Dispose() {
				Flush();
				mWriter.Dispose();
			}

			#endregion
		}

		/// <summary>
		/// Logger that logs to a given file stub name
		/// </summary>
		public class FileLogger : TWLogger {
			DirectoryInfo mLogDirectoryPath = null;
			public DirectoryInfo LogDirectoryPath {
				get {
					if (mLogDirectoryPath == null)
						mLogDirectoryPath = new DirectoryInfo(new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.FullName + @"\Logs");
					return mLogDirectoryPath;
				}

				set { mLogDirectoryPath = value; }
			}

			string FileStubName { get; set; }

			string mFileName = null;
			public string FileName {
				get {
					if (mFileName == null) {
						string fileStub = "";
						if (!string.IsNullOrEmpty(FileStubName))
							fileStub = FileStubName + "_";
						string aFileName = String.Format("{0}\\{1}{2}", LogDirectoryPath, fileStub, DateTime.Now.ToString("yyyy-MM-dd"));
						if(File.Exists(aFileName + ".txt"))	aFileName = String.Format("{0}({1})", aFileName, DateTime.Now.ToString("HH-mm"));
						mFileName = aFileName + ".txt";
					}
					return mFileName;
				}
				private set { mFileName = value; }
			}

			/// <summary>
			/// Constructor taking one filestub, will create in logs in a Logs directory at the same level as the executable.
			/// will catch exceptions and log as best it can - note can't log to the file as that is the problem...
			/// </summary>
			/// <param name="aFileStubName">The stub name of the log file to log to.</param>

			public FileLogger(string aFileStubName)
				: base() {
				FileStubName = aFileStubName;
				try {
					if (!LogDirectoryPath.Exists)
						LogDirectoryPath.Create();
					mWriter = new StreamWriter(FileName, true);
				}
				catch (Exception e) {
					string message = string.Format("Logging system failed to initialise for file {0} - Please ensure the program has rights to create files in the directory it is run from - Exiting Program", FileName);
					Console.WriteLine(message);
					throw new ApplicationException(message, e);
				}
			}

			#region IDisposable Members

			/// <summary>
			/// override of dispose to delete the file id it hasn't been used...
			/// </summary>
			public override void Dispose() {
				base.Dispose();
				if(!Used && File.Exists(FileName)) File.Delete(FileName);
			}

			#endregion
		}

		/// <summary>
		/// Class to allow more than one Log stream to be bundled as one.
		/// </summary>
		public class MultiLogger : ILoggable {
			List<ILoggable> mLoggers = new List<ILoggable>();

			/// <summary>
			/// Constructor taking any number of ILoggable items to instantiate the MultiLogger
			/// </summary>
			/// <param name="someLoggers"></param>
			public MultiLogger(params ILoggable[] someLoggers) {
				foreach (TWLogger logger in someLoggers)
					mLoggers.Add(logger);
			}

			/// <summary>
			/// method to add an Iloggable to the set of things this multilogger logs to.
			/// </summary>
			/// <param name="aLogger"></param>
			public void AddLogger(ILoggable aLogger) {
				mLoggers.Add(aLogger);
			}

			#region ILoggable Members

			public void Write(string message) {
				foreach (ILoggable logger in mLoggers)
					logger.Write(message);
			}

			public void Write(string format, params object[] args) {
				foreach (ILoggable logger in mLoggers)
					logger.Write(format, args);
			}

			public void WriteLine(string message) {
				foreach (ILoggable logger in mLoggers)
					logger.WriteLine(message);
			}

			public void WriteLine() {
				foreach (ILoggable logger in mLoggers)
					logger.WriteLine();
			}

			public void WriteLine(string format, params object[] args) {
				foreach (ILoggable logger in mLoggers)
					logger.WriteLine(format, args);
			}

			public void WriteStamped(string message) {
				foreach (ILoggable logger in mLoggers)
					logger.WriteStamped(message);
			}

			public void WriteStamped(string format, params object[] args) {
				foreach (ILoggable logger in mLoggers)
					logger.WriteStamped(format, args);
			}

			public void WriteStampedLine(string message) {
				foreach (ILoggable logger in mLoggers)
					logger.WriteStampedLine(message);
			}

			public void WriteStampedLine(string format, params object[] args) {
				foreach (ILoggable logger in mLoggers)
					logger.WriteStampedLine(format, args);
			}

			public void LogException(Exception error) {
				foreach (ILoggable logger in mLoggers)
					logger.LogException(error);
			}

			public void Flush() {
				foreach (ILoggable logger in mLoggers)
					logger.Flush();
			}

			#endregion

			#region IDisposable Members

			public void Dispose() {
				foreach (ILoggable logger in mLoggers) {
					logger.Dispose();
				}
			}
			#endregion
		}

		/// <summary>
		/// Low level class that logs to an internal string as provided to the constructor.
		/// </summary>
		public class StringLogger : TWLogger{
			public StringLogger(StringBuilder builder = null) : base() {
				if(builder == null)
					mWriter = new StringWriter();
				else
					mWriter = new StringWriter(builder);
			}
		}
}
